Keep DocumentDB Local state in sync, explain failures, and unify tree progress - #876
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors Local Quick Start state management so Docker/durable-store reconciliation becomes demand-driven (on first tree expansion or opening the webview) rather than running at extension activation, and it enriches the Connections tree presentation with more detailed tooltips and an explicit deep-refresh action.
Changes:
- Move Docker readiness checks behind
QuickStartService.checkDockerReadiness()and introduce lazy hydration (ensureHydrated) + explicit deep refresh (refreshHydratedState). - Update the Connections tree Quick Start node to start collapsed, hydrate on-demand, and add richer managed-instance tooltips (container + Docker host details).
- Add command contribution/tests for a single deep Refresh entry on the Quick Start root node and expand Jest coverage for the new hydration/refresh behaviors.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/webviews/documentdb/localQuickStart/localQuickStartRouter.ts | Switch Docker readiness probing from ContainerRuntime to QuickStartService.checkDockerReadiness. |
| src/webviews/documentdb/localQuickStart/localQuickStartRouter.test.ts | Update router mocks to cover the new Docker readiness entry point. |
| src/tree/connections-view/LocalQuickStart/revealQuickStartInstance.test.ts | Stabilize reveal tests by mocking lazy-hydration state. |
| src/tree/connections-view/LocalQuickStart/LocalQuickStartItem.ts | Implement lazy hydration on expansion, add deep refresh, and build enriched managed-instance tooltips. |
| src/tree/connections-view/LocalQuickStart/LocalQuickStartItem.test.ts | Add tests validating “collapsed + no Docker work until expansion” and hydration/probe behavior. |
| src/tree/connections-view/LocalQuickStart/LocalQuickStartItem.credentials.test.ts | Add tooltip assertions for retained Docker/container facts and 12-char container ID. |
| src/services/localQuickStart/QuickStartService.ts | Add tracing, cached Docker readiness snapshotting, lazy hydration, and shared reconciliation. |
| src/services/localQuickStart/QuickStartService.test.ts | Add tests for readiness retention, hydration concurrency/sharing, retryability, and refresh interactions. |
| src/documentdb/ClustersExtension.ts | Remove eager Quick Start reconciliation during activation; rely on lazy reconciliation. |
| src/commands/localQuickStart/openLocalQuickStart.ts | Await authoritative hydration before revealing the Quick Start webview. |
| src/commands/localQuickStart/openLocalQuickStart.test.ts | New test ensuring webview reveal waits for hydration. |
| src/commands/localQuickStart/contributions.test.ts | Validate the deep Refresh context menu appears exactly once on the Quick Start root. |
| package.json | Contribute a context-menu Refresh entry for the Quick Start root node. |
| l10n/bundle.l10n.json | Add localized strings used by the new tooltip/detail labels. |
Suppressed comments (1)
src/tree/connections-view/LocalQuickStart/LocalQuickStartItem.ts:74
- Execution-target labels in the tooltip should reuse existing localized strings. "Dev container" and "GitHub Codespaces" already have l10n keys (used in the Quick Start webview), but this code introduces a new "Dev Container" key and returns a raw "GitHub Codespaces" string, which bypasses localization and duplicates translation work.
function executionTargetLabel(readiness: DockerReadiness): string {
switch (readiness.executionTarget) {
case 'wsl':
return 'WSL';
case 'ssh':
return 'SSH';
case 'devContainer':
return l10n.t('Dev Container');
case 'codespaces':
return 'GitHub Codespaces';
case 'otherRemote':
A connection can fail for reasons that have nothing to do with the
database: a DocumentDB Local container was stopped, a Kubernetes
port-forward tunnel died, Atlas closed the TLS handshake. The driver
reports these as ECONNREFUSED, a server-selection timeout, or an OpenSSL
alert, none of which tell the user what to do.
Add ConnectionDiagnosticsService, a registry where the source that owns
the infrastructure explains the failure in its own words. It exposes a
single method, explain(clusterId, error) -> string | undefined, and
never throws: a failing provider is skipped and a stalling one is cut
off, so the caller can always still report the original error.
Providers translate only. They never show UI, recover, or retry. One
user action can run several database commands and several can fail at
once, so a provider with side effects would produce duplicate dialogs
and errors that are obsolete by the time they appear. Anything with a
side effect stays at the call site, which alone knows whether the user
is watching.
The error itself is never modified. A lot of code here inspects errors
by identity rather than text: instanceof UserCancelledError decides
failure versus cancellation, instanceof QueryError and SettingsHintError
change handling, error.code carries server and socket codes,
errorCodeExtractor reads error.cause.cause.code at a fixed depth,
extractErrorCode parses a prefix from the start of a message, and the
tRPC boundary rebuilds errors as { code, name, message, stack, cause }.
Leaving the error alone means none of that can break.
Three providers, each identifying its own clusters differently:
- Quick Start matches an in-memory instance list, then reuses
prepareForConnection, which also corrects a stale "Running" tree row.
- Kubernetes records clusterId while ensureReachable prepares the
connection, then checks whether the tunnel is still up.
- Atlas checks the error shape, then the mongodb.net host suffix, and
reuses the wording that previously existed only in the Discovery view.
Call sites are the places that render a failure: one central catch in
ConnectionsBranchDataProvider covering everything below a cluster, the
two modals in ClusterItemBase, the shell connect banner, and the query
playground. Background paths that show nothing are deliberately left
alone.
Also drop progress.md and work-summary.md, leftover scratch notes.
The connect-failure path wrote an error line and then immediately fired the close emitter, which disposes the terminal and takes the message with it. There is no VS Code setting to prevent that: for an extension-owned Pseudoterminal, onDidClose always disposes, and a non-zero exit code only adds a notification. Show a prompt instead of closing. ShellSessionManager.evaluate() re-runs initialize() whenever the session is uninitialized, which a failed connect leaves it as, so the next command the user types becomes the retry: start the container, press Enter, you are connected. A system line says so. The setEnabled(true) call that already sat immediately before the close now does something. Also report the failure through two channels that outlive the terminal: a notification, preferring the translated explanation, and an outputChannel.error line carrying the raw driver message plus the provider id and explanation, so a shared output channel is enough to diagnose a report remotely. The log line sits after extractErrorCode and before the SettingsHintError check, so the raw message stays intact for both.
Move the diagnostics catch from ConnectionsBranchDataProvider into BaseExtendedTreeDataProvider.wrapGetChildrenWithErrorAndStateHandling. The Discovery, Azure Resources (vCore and RU) and Azure Workspace providers all build on the same base method, so they now translate a failed expansion without any per-view wiring, and a future provider gets it for free. The Connections provider goes back to its original shape. Behaviour is unchanged: on failure we choose what to display and rethrow the original error object untouched, so telemetry and every downstream identity check keep working. Cluster nodes return error children rather than throwing, so they still handle their own failures in ClusterItemBase. Also record the clusterId to port-forward mapping in KubernetesResourceItem. The Discovery view calls ensureKubernetesPortForward directly instead of going through ConnectionReachabilityService, so rememberKubernetesCluster never ran for a Kubernetes connection opened from that view and KubernetesDiagnosticsProvider stayed silent for it.
Webviews still showed the raw driver error, because the tRPC boundary
rebuilds every error as { code, name, message, stack, cause }: a class,
a custom property or an extra cause level does not survive, so an
explanation cannot ride along on the error.
Add one shared procedure, common.explainOperationFailure, which reads
the clusterId from the webview's tRPC context and returns a translated
message or null. Any webview can ask; no per-procedure result fields and
no error wrapping. Wired into the Collection view query and the Query
Insights stage errors.
Named after the caller's situation rather than a cause. The providers
behind it explain a stopped container, a dead port-forward tunnel and an
Atlas TLS rejection today, and can explain other infrastructure later
without the name becoming wrong.
Passing only the message is enough for all three providers: Quick Start
inspects container state and ignores the error, Kubernetes checks the
tunnel and then regexes the message, Atlas regexes the message and
checks the host suffix. ConnectionDiagnosticsRequest.error is already
unknown, so a string needs no signature change. The limitation is
recorded in the skill: a provider needing an error's class or code
cannot be served from a webview.
Tree-node commands (create and drop database, collection and index, and anything else registered with registerCommandWithTreeNodeUnwrappingAndModalErrors) reported the raw driver error. The unwrapped node carries the cluster, so one catch in commandErrorHandling covers all of them: on a translated failure we suppress the default notification and show the explanation with the raw error as detail. UserFacingError keeps its existing path, since it already carries a deliberate message. The Document view's three failure paths now ask common.explainOperationFailure, the same way the Collection view does. Its tRPC context already carries a clusterId, so no plumbing was needed. As everywhere else, the error object is passed through untouched.
Lazy hydration made a Docker discovery failure fatal: listByLabel rejects with no docker binary or a stopped daemon, and both entry points awaited ensureHydrated() unguarded. The tree row rendered empty behind an error toast, and the webview - the one surface that can diagnose Docker - never opened.
…re failure registerCommandWithTreeNodeUnwrappingAndModalErrors calls explain() for every error that is not a UserFacingError, and both the Quick Start and Kubernetes providers can answer without inspecting the error. Escaping a wizard on a stopped managed instance therefore raised a modal saying DocumentDB Local was not running. Guarded once in the service so every call site inherits it.
…ntainer inspectContainer reports "could not ask" and "not there" identically, so stopping Docker Desktop made the preflight assert the container had been removed outside VS Code and recommend recreating it. The preflight now confirms the daemon before concluding missing, and a new dockerUnreachable verdict carries wording that matches what actually happened.
prepareForConnection() corrected state, fired the status emitter and could warn, so the error-translation provider was repairing state and repainting the tree while claiming to only translate. Split the verdict out as inspectManagedInstance() and left the side effects on the connection path.
A diagnosis becomes the heading of a modal, so the four-line bulleted Atlas text rendered as a block of bold lines above a one-line detail. The provider now returns a single-paragraph summary; the Discovery-view modal keeps the long form where it belongs, in the detail area.
MessageOptions.detail is only rendered for modal messages, so suppressing the default notification and passing the driver text as detail hid it entirely. Appended to the message instead, matching displayErrorMessage.
The connect-failure line goes to an output channel the user is encouraged to share, and a driver error can quote the connection string, which for a Quick Start instance carries a generated password.
Expanding the managed cluster raised a modal that blocked the expansion until it was answered and then left the node empty, and every other non-ready verdict rendered as silence. Each verdict now gets an actionable row, which also removes the module-level prompt singleton that would have been shared across aliases.
Each provider had its own 5s race, so three registered sources could hold an error back for 15s on top of the driver's own timeout.
The tooltip surfaced raw identifiers (NotInstalled, unixSocket, linux) next to already-humanised provider and target labels. Markdown escaping is also narrowed to the characters that change rendering, so versions and image refs read plainly.
Moving unwrapArgs out of the try meant a throw from unwrapping bypassed the UserFacingError handling.
The context-menu refresh shells out to Docker with no feedback; the view now carries the spinner.
…w path Nothing in the request type stopped a future provider from reaching for instanceof or .code and silently never matching from a webview.
The Quick Start rows drew their own `loading~spin` icons and a "· Refreshing…" text hint instead of using `ext.state`, so progress looked and behaved differently from every other node in the tree. The work is service-owned — it can start from the webview, a lifecycle command, or the background probe — so the row had nothing to await. QuickStartService now publishes an awaitable handle for in-flight work (`getInFlightOperation`) plus a dedicated `onDidChangeOperation` event, kept separate from `onDidChangeStatus` because that one's listeners rebuild the whole Connections view. A new bridge maps that handle to `ext.state.runWithTemporaryDescription` on the instance row. It is deferred through a microtask, since work can start from inside a tree render and applying state fires a refresh synchronously. The lifecycle commands are deliberately NOT wrapped as well: the bridge already covers every origin, and two owners would clip the indicator early. The transitional rows keep their `state_*` context values — the framework overlays description and icon but never `contextValue`, which the lifecycle menus gate on. Also replaces the view-level progress in the node's explicit refresh with node progress, and drops the synthetic "busy" child row in favour of showing progress on the node itself. The Provisioning row keeps its own spinner: there is no instance row to attach node progress to yet, and it mirrors what `ext.state.showCreatingChild` renders.
Starting / Stopping are only ever set inside runLifecycle and are always superseded before it returns, so the row text was unreachable — the bridge overlay replaces description and icon for the whole transition. Two versions of the same sentence could only ever drift apart, so the rows now render exactly what the overlay does. The host is dropped from those rows: it is not actionable mid-transition and it is still one hover away in the tooltip. Provisioning splits the host out of its localized string as well, so the "· localhost:" separator is no longer something translators can reorder or drop.
"Running · localhost:10260" and its Stopped twin embedded a non-translatable
host and separator in a localized string, where a translator could reorder or
drop them. The state half now comes from instanceStateLabel — the same function
the tooltip uses — so the row and the tooltip can no longer disagree about what
state the instance is in.
The remaining "·" strings are prose on both sides ("Missing · click to
recreate"), so they stay whole; splitting them would hand translators fragments
with no context.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 47 out of 47 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/tree/connections-view/LocalQuickStart/LocalQuickStartItem.ts:395
- The background freshness path can still turn a Docker outage into a false
Missingstate.refreshLiveState()treatsinspectContainer() === undefinedas “gone” (QuickStartService.ts:1827-1843), butinspectContaineralso returnsundefinedwhen Docker cannot be queried (ContainerRuntime.ts:238-245). After a hydrated node is expanded with Docker stopped, this call therefore shows the recreate guidance that the new preflight classification was intended to avoid. Please make the background refresh distinguish daemon-unreachable from confirmed absence before updatingmissing.
src/utils/commandErrorHandling.ts:154 - This does not cover the database tree-node commands claimed in the PR description. The create/drop/import/export/index commands are still registered with plain
registerCommandWithTreeNodeUnwrappingin ClustersExtension.ts:1009-1067; the diagnostics-aware wrapper is only used for connection-management commands. Those database failures therefore continue to surface the default raw driver error. Please route the actual database command registrations through diagnostics-aware handling (without changing their existing modal semantics).
src/services/connectionDiagnosticsService.ts:159 - Racing
askProviders()against a timer does not stop the provider loop. If a provider settles after 5 seconds, the loop continues querying later providers and can emitconnectionDiagnostics.explainedeven though the caller already receivedundefined; this both performs post-deadline I/O and records a false “explained” event. Propagate an expiry/cancellation signal into the loop and prevent later providers and telemetry after the deadline.
return withDeadline(this.askProviders(request));
src/documentdb/shell/DocumentDBShellPty.ts:553
- Only initial shell connection failures use diagnostics. If the container is stopped after a shell session was established, the next command fails through
handleEvalError()(DocumentDBShellPty.ts:721-750), which still prints only the raw driver error. That leaves the shell only partially covered despite the PR’s stated surface coverage. Please apply the same translation to evaluation/reconnect failures while preserving the existing error-code and SettingsHintError handling.
const diagnosis = await ConnectionDiagnosticsService.explain({
clusterId: this._connectionInfo.clusterId,
error,
});
UX review item 20: the managed instance row looked like a cluster but carried none of the cluster commands, so New Database, Launch Shell and Refresh were missing and Copy Connection String had to be duplicated as a Quick Start command. The row deliberately does not take `treeItem_documentdbcluster`. That tag gates thirteen entries, six of which resolve the node through connection storage — rename, move, remove, update credentials, update connection string — and the managed instance has no storage record for them to act on. Granting the tag and excluding the six would mean every future cluster command reaches this row by default, which is the wrong way round for a node that is not a stored connection. Commands are opted in individually instead, gated to `state_running` because every other state renders a plain row with no `cluster` to dereference. The contribution test pins both halves: the three that must appear, and the six that must not. Copy Connection String and Copy Password stay as Quick Start commands. They are also offered while the instance is stopped, where the row is not a cluster item at all and the generic command would have no `getCredentials()` to call.
Fixes #865. `QuickStartService` emitted user-facing text on `StageEvent.message` / `StageEvent.error` and `QuickStartStatus.errorMessage`, so the service layer owned copy, and every new message was one more chance to forget `l10n.t`. Five of them had already forgotten: the stage payloads `'Checking Docker…'`, `'Pulling the official image…'`, `'Creating container…'`, `'Starting container…'` and `'Waiting for DocumentDB to accept connections…'` were raw English. They were also dead. The webview labels the checklist from its own `stageLabels()` map and only reads `message` on terminal events, so those five strings were never rendered — untranslated text that nobody could have reported, because nobody could see it. They are gone rather than localized. What remains is a `QuickStartMessage`: a key, plus the data needed to phrase it (`port`, `environment`) and a `detail` field carrying raw daemon or driver text. `detail` is the one thing never translated, because it is evidence rather than copy — and keeping it in its own field is what stops a daemon string being concatenated into a sentence a translator owns. `StageEvent.error` is gone too. It duplicated `message` at every call site except two, where it differed only by being undefined on abort, which the webview then fell back out of. One field and `status` say the same thing. The wording lives in one shared `formatQuickStartMessage`, not one map per surface. Two copies of the same sentence in the tree and the webview could only drift, which is the failure this repo just fixed for the transitional rows. Tests now assert keys instead of sentences, which is what #764 asks for. The M5 regression — the daemon's "Bind for …" text must not reach the user — is now structural: a keyed message has nowhere to put it, and the test pins that `detail` is absent rather than grepping the rendered string.
…nd detail Two findings from the #879 review, both real. A second readiness timeout in `resumeReadiness` fell through to `unexpectedFailure` and put the raw `ReadinessTimeoutError` text on screen — the one thing this PR set out to stop. It now reports `readinessTimeout` carrying the host environment, so a repeat timeout gets the same dev-container port-routing explanation as the first one; `stillInitializing` stays for a cancelled wait and `unexpectedFailure` for a genuine finalize error. `formatQuickStartMessage` trimmed `detail` and then returned it through `??`, so a whitespace-only detail rendered as an empty message. Detail now collapses to undefined when it carries no evidence, and `unexpectedFailure` keeps a localized sentence around the raw text instead of replacing the copy with it — which is what the field was documented to do. Covered by a new test for the formatter: every key renders something, raw driver text never stands alone, and whitespace-only detail can never blank the message.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 52 out of 52 changed files in this pull request and generated no new comments.
Suppressed comments (6)
src/webviews/documentdb/documentView/documentView.tsx:191
- If explainOperationFailure.query() rejects, this async .catch handler will throw and suppress the user-facing error notification. The explanation query should be best-effort and never block the fallback error message.
src/webviews/documentdb/documentView/documentView.tsx:242 - If explainOperationFailure.query() rejects, this async .catch handler will throw and the save failure may not be surfaced. The explanation query should be best-effort and fall back to the original localized message.
src/webviews/documentdb/collectionView/queryInsightsTab/QueryInsightsTab.tsx:264 - The new explainOperationFailure call is not failure-tolerant: if the tRPC query rejects (e.g., extension host busy/reloading), the .then() chain is skipped and no user-visible error message is shown. This is a regression from the previous always-show behavior.
src/webviews/documentdb/documentView/documentView.tsx:86 - If explainOperationFailure.query() rejects, this async .catch handler will throw and no error message will be displayed to the user. The error-explanation step should be best-effort and fall back to the original localized message.
This issue also appears in the following locations of the same file:
- line 187
- line 238
src/webviews/documentdb/collectionView/CollectionView.tsx:408
- If explainOperationFailure.query() rejects, this async .catch handler will throw and the query failure may not be shown to the user. The explanation step should be best-effort and fall back to the original localized message.
l10n/bundle.l10n.json:1299 - This string has corrupted apostrophes ("machineȁs", "projectȁs"), which will show up incorrectly in the UI. It should use the intended right single quotation mark (’) or a plain apostrophe.
"MongoDB Atlas closed the TLS connection with an internal error. That is a transport-level rejection rather than a failed sign-in, so it is worth checking whether this machineȁs IP address is on the projectȁs IP access list, and whether the cluster is paused.": "MongoDB Atlas closed the TLS connection with an internal error. That is a transport-level rejection rather than a failed sign-in, so it is worth checking whether this machineȁs IP address is on the projectȁs IP access list, and whether the cluster is paused.",
…overage All four were real, and three of them were places where this PR claimed a surface it had not actually reached. **A Docker outage still read as a deleted container.** The preflight learned to tell "could not ask" from "not there" (`classifyUninspectableContainer`), but the background freshness probe kept its old logic and set `missing` on any empty inspect. Stopping Docker Desktop under an expanded node therefore offered to recreate a container that was sitting on disk, untouched — the exact wrong turn the preflight work existed to prevent. `refreshLiveState()` now confirms the daemon is answering before concluding anything, and keeps the last known state otherwise. **The database commands were never wired up.** The description claimed tree-node commands were covered; only the connection-management ones were. Rather than reuse the modal wrapper, which would have turned every failed drop or import into a blocking dialog, this adds `registerCommandWithTreeNodeUnwrappingAndDiagnostics`: same translation, reported the way those commands already report failures. Nine registrations move over. **The explain deadline did not stop anything.** It was a `Promise.race`, so the losing side kept going: later providers were still queried, and a provider that answered after the caller had been handed `undefined` still recorded `connectionDiagnostics.explained`. The deadline is now an `AbortSignal` the loop checks before each provider and again after each await. **The shell was only half covered.** A session that connects fine can still break underneath the user — the container stops, a port-forward drops — and the next command is where they find out. That path printed the raw driver error. `handleEvalError` now asks for a diagnosis too, keeping the existing error-code and SettingsHintError handling intact. Also localizes the tooltip's product labels, which were raw strings while the webview localizes the same words, and drops a duplicate "Dev Container" key that differed from the webview's "Dev container" only by case.
|
Replying to the four suppressed comments in review #4892351921 — they have no threads of their own, so they get one comment here. All four were correct, and three of them were places where this PR's description claimed a surface it had not actually reached. Fixed in 1.
|
✅ Code Quality Checks
This comment is updated automatically on each push. |
📦 Build Size Report
Download artifact · updated automatically on each push. |
Fixes #873
Problem
Stopping DocumentDB Local outside VS Code left the extension believing it was still running, and the resulting connection failure surfaced as a raw driver error (
ECONNREFUSED, a server selection timeout) that said nothing about the container. The reporter asked for two things: validate the container on connect, and recover from, or at least explain, the failure afterwards.The first half was straightforward. The second half turned out to be a general problem: a connection can fail for reasons that have nothing to do with the database, and we had no way for the component that owns that infrastructure to say so.
What this does
Quick Start state accuracy
Runningstate can never reach the database client. A stopped instance offers to start.Error translation (
ConnectionDiagnosticsService)A registry where the source that owns the infrastructure explains a failure in its own words. Providers ship for DocumentDB Local (Docker), Kubernetes port forwarding, and Atlas TLS rejections.
It is deliberately translation only. Providers return text; they never show UI, never recover, never retry, and never touch the error object. That last constraint is load-bearing: a lot of code here inspects errors by identity rather than text, and all of it would break silently otherwise.
Why the error object is never modified
instanceof UserCancelledErrordecides failure versus cancellation, in roughly 25 placesinstanceof QueryError,MongoBulkWriteErrorandSettingsHintErrorchange how a failure is handlederror.codecarries server codes (115, 235) and socket codes (ECONNRESET,ENOTFOUND)errorCodeExtractor.tsreadserror.cause.cause.codeat a fixed depth, so an extra wrapper level breaks Collection view error-code detectionextractErrorCode()parses a[CODE-12345]prefix from the start of a message, so prepending text breaks the shell and the playground{ code, name, message, stack, cause }, so a custom property never reaches a webview anywayCovered surfaces. Cluster connect and list databases in all views; everything below a cluster in all four tree views, via one catch in
BaseExtendedTreeDataProvider; the Collection view query and Query Insights; the Document view; tree-node commands; the shell; the query playground. Background paths that show nothing (count badges, prefetches) are deliberately left alone.Webviews ask through a single shared procedure,
common.explainOperationFailure, since an explanation cannot ride along on an error across the tRPC boundary.Shell terminal no longer disappears on a failed connect
The connect-failure path wrote an error line and then immediately fired the close emitter, which disposes the terminal and takes the message with it. There is no VS Code setting for this: for an extension-owned
Pseudoterminal,onDidClosealways disposes.It now shows a prompt instead.
ShellSessionManager.evaluate()re-runsinitialize()while the session is uninitialized, which a failed connect leaves it as, so the next command the user types becomes the retry: start the container, press Enter, you are connected. The failure is also reported through two channels that outlive the terminal, a notification and anoutputChannel.errorline carrying the raw driver message plus the provider id and explanation, so a shared output channel is enough to diagnose a report remotely.Message style
Messages avoid asserting what happened, since we cannot know. "We cannot find the DocumentDB Local container. It was very likely removed outside VS Code. You can recreate it from the Connections view, which reuses the existing data volume."
Documentation
.github/skills/error-translation/SKILL.mdcovers the one rule (providers translate, never show UI), the three ways a provider identifies its own clusters, why verdicts must not be cached, the identity checks that forbid touching errors, which background paths to leave alone, and the message conventions.Follow-up
#877 captures ideas for where this could go next (recovery buttons, self healing, proactive detection), explicitly as ideas needing discussion rather than a plan. It is labelled
on-holdandneeds-triage.Not included here: an Azure diagnostics provider, so Azure firewall and IP-rule rejections still surface raw. Noted in #877.
Review follow-ups
A review pass over the branch turned up a set of defects. Each is fixed here in its own commit.
Two were user-visible regressions.
Making hydration lazy also made it fatal.
listByLabelrejects when there is nodockerbinary orthe daemon is stopped, and both Quick Start entry points awaited
ensureHydrated()unguarded, sothe tree row rendered empty behind an error toast and the webview, the one surface that can diagnose
Docker, never opened. Both call sites now tolerate the rejection; the service stays retryable.
registerCommandWithTreeNodeUnwrappingAndModalErrorsasked for an explanation on every error thatwas not a
UserFacingError, and a provider is allowed to answer without inspecting the error atall. Escaping a wizard on a stopped instance therefore raised a modal saying DocumentDB Local was
not running.
explain()now returnsundefinedfor aUserCancelledErrorbefore any provider isasked.
The rest
inspectContainerreports "could not ask" and "not there" identically, so stopping Docker Desktopmade the preflight assert the container had been removed outside VS Code and recommend recreating
it. The preflight confirms the daemon before concluding
missing, and a newdockerUnreachableverdict carries wording that matches what actually happened.
prepareForConnection()corrected state, fired the status emitter and could warn, so theerror-translation provider was repairing state and repainting the tree while claiming to only
translate. The verdict is split out as a read-only
inspectManagedInstance().block of bold lines. The provider now returns a single-paragraph summary; the Discovery-view modal
keeps the long form where it belongs, in the detail area.
MessageOptions.detailis only rendered for modal messages, so the tree base class wassuppressing the default notification and then hiding the driver text entirely. It is appended to
the message instead, matching
displayErrorMessage.driver error can quote the connection string, which for a Quick Start instance carries a generated
password. Cached secrets are redacted first.
then left the node empty, and every other non-ready verdict rendered as silence. Each verdict now
gets an actionable row, which also removes a module-level prompt singleton that would have been
shared across aliases.
getChildren()once hydration has completed,and the background-probe cooldown was still unarmed, so the very first expansion started a
redundant
docker inspectand flashedRefreshing…on the row. Hydration now arms the cooldown,as
refreshHydratedState()already did.explain()deadline was per provider, so three registered sources could hold an error backfor 15s on top of the driver's own timeout. It is now one budget for the whole call.
unwrapArgsmoved back inside the guarded block, display labels instead of rawidentifiers (
NotInstalled,unixSocket) in the tooltip, narrower markdown escaping, andprogress during the explicit deep refresh.
Two findings were left alone on purpose. Collapsing the Quick Start root is what makes hydration
lazy, and the Quick Start provider ignoring the error shape is the premise that lets it answer at
all; both are recorded with their reasoning rather than silently dropped.
The review and its resolution live in
docs/ai-and-plans/PRs/876-quickstart-error-translation-review.md.One way to show progress in the tree
Quick Start drew its own
loading~spinicons and a· Refreshing…text hint instead of the treeframework's node progress (
ext.state), so one view expressed the same idea two ways. Three ofthose sites were also plain gaps: starting or stopping from the tree gave no feedback at all until
the service pushed a new state, and the node's own refresh spun the view title rather than the node.
The obstacle was that the work is service-owned. A start can come from the Quick Start webview, a
lifecycle command, or the background freshness probe, so the row had nothing to await.
QuickStartServicenow publishes an awaitable handle for in-flight work (getInFlightOperation)alongside a dedicated
onDidChangeOperationevent, kept separate fromonDidChangeStatusbecausethat one's listeners rebuild the whole Connections view. A small bridge maps the handle onto
ext.state.runWithTemporaryDescriptionfor the instance row.· Refreshing…hint are gone. The rowis decorated the way a deleting collection or an importing database already is.
state_*context values. The framework overlays description andicon but never
contextValue, which the lifecycle menus gate on.state fires a refresh synchronously.
origin, and two owners of one indicator would clip it early.
written by another window or a previous session, so there is no promise to await and no instance
row to attach to yet. The call site records that reasoning.
Transitional rows and the overlay now render identical strings, with the host dropped from both: it
is not actionable mid-transition, and two copies of one sentence can only drift apart. Where a host
does belong, as in
Running · localhost:10260, it is composed outside the localized string so theseparator is not something a translation can reorder, and the state half comes from the same
function that builds the tooltip.
New tests cover the operation handle in the service and the bridge itself: the correct row and
label per operation kind, no stacking while one operation runs, pickup of the next operation, and
silence when nothing is in flight.
Cluster commands on the managed instance
The running instance row looked like a cluster but carried none of the cluster commands, so New
Database, Launch Shell and Refresh were missing, and Copy Connection String had to exist twice.
That is UX review item 20, from #790.
The row deliberately still does not take
treeItem_documentdbcluster. That tag gates thirteenentries, six of which resolve the node through connection storage — rename, move, remove, update
credentials, update connection string, plus Azure data migration — and a service-owned instance has
no storage record for them to act on. Granting the tag and excluding the six would mean every
future cluster command reaches this row by default, which is the wrong default for a node that is
not a stored connection.
Commands are opted in individually instead, gated to
state_runningbecause every other staterenders a plain row with no
clusterto dereference. The contribution test pins both directions:the three that must appear, the six that must not, and a guard that the tree item never starts
using
CLUSTER_ITEM_CONTEXT_VALUE.Copy Connection String and Copy Password stay as Quick Start commands rather than folding into the
generic ones: they are also offered while the instance is stopped, where the row is not a
cluster item at all and the generic command would have no
getCredentials()to call.Second review pass
A later review found four more defects. All four were real, and three were surfaces this description claimed but the code had not reached. Each is fixed in
0194ac26.missingon any empty inspect. Stopping Docker Desktop under an expanded node offered to recreate a container that was sitting on disk.refreshLiveState()now confirms the daemon is answering first, and keeps the last known state otherwise.UserFacingErrorfrom nine commands into a blocking dialog, so there is a newregisterCommandWithTreeNodeUnwrappingAndDiagnosticsinstead: same translation, reported the way those commands already report failures.Promise.raceabandons the losing promise rather than cancelling it, so later providers were still queried and a late answer still recordedconnectionDiagnostics.explainedfor a caller that had already been handedundefined. The deadline is now anAbortSignalchecked before each provider and again after each await.handleEvalErrornow asks for a diagnosis too, keeping theSettingsHintErrorhint and the error-code-50 tip intact. It becameasyncas a result, so a failed command can wait on a diagnosis before the prompt returns — bounded by the 5s deadline.Also from the first review: the tooltip's product labels were raw strings while the webview localizes the same words, and
'Dev Container'differed from the webview's'Dev container'by one capital letter, which extracted as a second key. Both fixed; net zero new strings.Validation
npm run l10nnpm run prettier-fixnpm run lint— cleannpx jest --no-coverage— 218 suites, 3,499 testsnpm run build— cleanNew tests cover the service (first answer wins, throwing and stalling providers are skipped, the error comes back untouched), each of the three providers, and the tree base class translation including a regression guard that background count paths never invoke it.